package org.example.project.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import groovy.util.logging.Slf4j;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.example.project.common.convention.exception.ClientException;
import org.example.project.common.convention.exception.ServiceException;
import org.example.project.config.GotoDomainWhiteListConfiguration;
import org.example.project.dao.entity.*;
import org.example.project.dao.mapper.*;
import org.example.project.dto.req.ShortLinkCreatReqDTO;
import org.example.project.dto.req.ShortLinkPageReqDTO;
import org.example.project.dto.req.ShortLinkUpdateReqDTO;
import org.example.project.dto.resp.GroupShortLinkCountRespDTO;
import org.example.project.dto.resp.ShortLinkCreateRespDTO;
import org.example.project.dto.resp.ShortLinkPageRespDTO;
import org.example.project.service.ShortLinkService;
import org.example.project.toolkit.HashUtil;
import org.example.project.toolkit.ShortLinkUtil;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;
import java.net.URL;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import static org.example.project.common.constant.RedisKeyConstant.*;
import static org.example.project.common.constant.ShortLinkConstant.DEFAULT_VALIDATE_TIME;
import static org.example.project.common.constant.ShortLinkConstant.UV_COOKIE_NAME;
import static org.example.project.common.enums.ValidDateTypeEnum.CUSTOM;
import static org.example.project.common.enums.errorEnum.ShortLinkErrorCodeEnum.*;

/**
 * @author weibin
 * @since 2025/10/5 0:11
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class ShortLinkServiceImpl extends ServiceImpl<ShortLinkMapper, ShortLinkDO> implements ShortLinkService {

    private final RBloomFilter<String> shortLinkCreateCachePenetrationBloomFilter;
    private final LinkGotoMapper linkGotoMapper;
    private final StringRedisTemplate stringRedisTemplate;
    private final RedissonClient redissonClient;
    private final LinkAccessStatsMapper linkAccessStatsMapper;
    private final LinkLocaleStatsMapper linkLocaleStatsMapper;
    private final LinkOsStatsMapper linkOsStatsMapper;
    private final LinkBrowserStatsMapper linkBrowserStatsMapper;
    private final LinkAccessLogsMapper linkAccessLogsMapper;
    private final LinkDeviceStatsMapper linkDeviceStatsMapper;
    private final LinkNetworkStatsMapper linkNetworkStatsMapper;
    private final GotoDomainWhiteListConfiguration gotoDomainWhiteListConfiguration;

    @Value("${gaoDe.ipService.url}")
    private String gaoDeIpServiceUrl;

    @Value("${gaoDe.ipService.key}")
    private String gaoDeIpServiceKey;

    @Value("${short-link.default-domain}")
    private String defaultDomain;


    @Override
    @Transactional(rollbackFor = Exception.class)
    public ShortLinkCreateRespDTO createShortLink(ShortLinkCreatReqDTO requestParam) {
        ShortLinkDO shortLinkDO = BeanUtil.copyProperties(requestParam, ShortLinkDO.class);

        // 设置默认域名
        shortLinkDO.setDomain(defaultDomain);

        // 生成短链接
        String shortLinkSuffix;
        String fullShortUrl;

        // 保证生成的短链接全路径全局唯一
        int genCount = 0;
        do {
            shortLinkSuffix = HashUtil.hashToBase62(requestParam.getOriginUrl());
            fullShortUrl = shortLinkDO.getDomain() + "/" + shortLinkSuffix;
            genCount++;
        } while (shortLinkCreateCachePenetrationBloomFilter.contains(fullShortUrl) && genCount < 10);

        if (genCount >= 10) {
            throw new ServiceException(SHORT_LINK_GEN_TOO_MANY_ERROR);
        }

        shortLinkDO.setShortUri(shortLinkSuffix);
        shortLinkDO.setFullShortUrl(fullShortUrl);
        shortLinkDO.setFavicon(this.getFaviconByUrl(requestParam.getOriginUrl()));
        shortLinkDO.setEnableStatus(0);
        shortLinkDO.setDelFlag(0);

        // 写入完整短链接个gid的跳转表，维护一个关系
        LinkGotoDO linkGotoDO = LinkGotoDO.builder()
                .fullShortUri(fullShortUrl)
                .gid(requestParam.getGid())
                .build();

        // 布隆过滤器存在一定概率误判，所以必须给短链接全路径加索引，以此作为兜底策略
        try {
            baseMapper.insert(shortLinkDO);
            linkGotoMapper.insert(linkGotoDO);
        } catch (DuplicateKeyException e) {
            throw new ServiceException(SHORT_LINK_DUPLICATE_ERROR);
        }

        // 纳入布隆过滤器管理
        shortLinkCreateCachePenetrationBloomFilter.add(fullShortUrl);

        // 缓存预热（这里最好是设置一个30天以内的随机有效期，避免缓存雪崩，也避免数据一直存在于缓存里）
        stringRedisTemplate.opsForValue()
                .set(
                        String.format(SHORT_LINK_GOTO_KEY, fullShortUrl),
                        shortLinkDO.getOriginUrl(),
                        ShortLinkUtil.getValidaTime(shortLinkDO.getValidDate()),
                        TimeUnit.SECONDS);

        return ShortLinkCreateRespDTO.builder()
                .gid(shortLinkDO.getGid())
                .originUrl(shortLinkDO.getOriginUrl())
                .fullShortUrl(shortLinkDO.getFullShortUrl())
                .build();
    }

    /**
     * 根据网站 url 获取网站图标
     *
     * @param url 网站 url
     * @return  网站图标路径
     */
    private String getFaviconByUrl(String url) {
        try {
            Document doc = Jsoup.connect(url)
                    .userAgent("Mozilla/5.0")
                    .timeout(10000)
                    .get();

            // 查找所有可能的图标链接
            Element faviconLink = doc.select("""
                link[rel~=icon], 
                link[rel~=shortcut\\ icon], 
                link[rel~=apple-touch-icon]
                """).first();

            if (faviconLink != null) {
                return faviconLink.attr("abs:href");
            }

            // 返回默认图标地址
            return new URL(url).getProtocol() + "://" + new URL(url).getHost() + "/favicon.ico";

        } catch (Exception e) {
            return null;
        }
    }

    @Override
    public IPage<ShortLinkPageRespDTO> pageShortLink(ShortLinkPageReqDTO requestParam) {
        LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                .eq(StrUtil.isNotBlank(requestParam.getGid()), ShortLinkDO::getGid, requestParam.getGid())
                .eq(ShortLinkDO::getEnableStatus, 0)
                .eq(ShortLinkDO::getDelFlag, 0)
                .orderByDesc(ShortLinkDO::getUpdateTime);
        ShortLinkPageReqDTO page = this.page(requestParam, queryWrapper);
        return page.convert(x -> {
            ShortLinkPageRespDTO shortLinkPageRespDTO = BeanUtil.copyProperties(x, ShortLinkPageRespDTO.class);
            shortLinkPageRespDTO.setDomain("http://" + shortLinkPageRespDTO.getDomain());
            return shortLinkPageRespDTO;
        });
    }

    @Override
    public List<GroupShortLinkCountRespDTO> listGroupShortLinkCount(List<String> requestParam) {
        if (CollUtil.isEmpty(requestParam)) {
            return Collections.emptyList();
        }
        QueryWrapper<ShortLinkDO> queryWrapper = Wrappers.query(new ShortLinkDO())
                .select("gid as gid, count(*) as shortLinkCount")
                .eq("enable_status", 0)
                .eq("del_flag", 0)
                .in("gid", requestParam)
                .groupBy("gid");
        List<Map<String, Object>> maps = this.listMaps(queryWrapper);
        return BeanUtil.copyToList(maps, GroupShortLinkCountRespDTO.class);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateShortLink(ShortLinkUpdateReqDTO requestParam) {

        // 白名单检查
        this.checkWhite(requestParam.getOriginUrl());

        LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                .eq(ShortLinkDO::getGid, requestParam.getOriginGid())
                .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                // 主要是根据上面这两个信息来唯一定位这个短链接数据记录
                .eq(ShortLinkDO::getEnableStatus, 0)
                .eq(ShortLinkDO::getDelFlag, 0);
        ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper);
        if (shortLinkDO == null) {
            throw new ClientException(SHORT_LINK_NULL);
        }

        /*
        如果修改了短链接的gid，也就是修改了短链接的所属分组，那需要删除原来其所在的表记录，插入新的表
         */
        ShortLinkDO updateDO = new ShortLinkDO();
        updateDO.setOriginUrl(requestParam.getOriginUrl());
        updateDO.setValidDateType(requestParam.getValidDateType());
        if (Objects.equals(requestParam.getValidDateType(), CUSTOM.getCode())) {
            updateDO.setValidDate(requestParam.getValidDate());
        }
        updateDO.setDescribe(requestParam.getDescribe());
        
        if (!Objects.equals(requestParam.getOriginGid(), requestParam.getGid())) {
            updateDO.setDelFlag(1);
        }

        baseMapper.update(updateDO, queryWrapper);
        
        if (!Objects.equals(requestParam.getOriginGid(), requestParam.getGid())) {
            ShortLinkDO insertDO = BeanUtil.copyProperties(shortLinkDO, ShortLinkDO.class);
            insertDO.setGid(requestParam.getGid());
            insertDO.setOriginUrl(requestParam.getOriginUrl());
            insertDO.setValidDateType(requestParam.getValidDateType());
            if (Objects.equals(requestParam.getValidDateType(), CUSTOM.getCode())) {
                insertDO.setValidDate(requestParam.getValidDate());
            }
            insertDO.setDescribe(requestParam.getDescribe());
            insertDO.setDelFlag(0);
            baseMapper.insert(insertDO);
        }

        // TODO:修改短链接分组后，其他表里只要是由gid和full_short_link这俩字段的数据都需要同步进行修改以保证数据一致性

        /*
        修改短链接时维护缓存：
        1. 主要是修改短链接的有效期，这个需要进行维护短链接在redis里缓存数据
         */
        if (!ObjUtil.equals(shortLinkDO.getValidDateType(), requestParam.getValidDateType())
                || !ObjUtil.equals(shortLinkDO.getValidDate(), requestParam.getValidDate())) {
            // 修改了有效期，那就直接把缓存删了就行,下次路由时再加载到redis里就行
            stringRedisTemplate.delete(String.format(SHORT_LINK_GOTO_KEY, shortLinkDO.getFullShortUrl()));
        }

    }

    /**
     * 检查原始链接是否在白名单内
     *
     * @param originUrl 原始链接
     */
    private void checkWhite(String originUrl) {
        Boolean enable = gotoDomainWhiteListConfiguration.getEnable();
        if (enable == null || !enable) {
            return;
        }
        String domain = ShortLinkUtil.extractDomain(originUrl);
        if (StrUtil.isBlank(domain)) {
            throw new ClientException("跳转链接填写错误");
        }
        List<String> details = gotoDomainWhiteListConfiguration.getDetails();
        if (!details.contains(domain)) {
            throw new ClientException("目前只允许跳转以下域名：" + gotoDomainWhiteListConfiguration.getNames());
        }
    }

    @Override
    public void routerShortUri(String shortUri, ServletRequest request, ServletResponse response) throws IOException {
        // 获取完整短链接
        String serverName = request.getServerName();
        String port = Optional.of(request.getServerPort())
                .filter(each -> !ObjUtil.equals(80, each))
                .map(each -> ":" + each)
                .orElse("");
        String fullShortUrl = serverName + port + "/" + shortUri;

        // 去redis缓存里取该短链接对应原始链接
        String originalUrl = stringRedisTemplate.opsForValue().get(String.format(SHORT_LINK_GOTO_KEY, fullShortUrl));
        if (StrUtil.isNotBlank(originalUrl)) {

            statsShortLink(null, fullShortUrl, request, response);

            // 302到原始链接
            ((HttpServletResponse) response).sendRedirect(originalUrl);
            return;
        }

        /*
        缓存击穿问题
        redis 缓存里也没有时，去布隆过滤器查该链接是否存在，创建短链接时我们维护了
        布隆过滤器判断不存在，那就一定不存在
        布隆过滤器判断存在，那大概率存在，小概率误判，可能不存在
         */
        boolean contains = shortLinkCreateCachePenetrationBloomFilter.contains(fullShortUrl);
        if (!contains) {
            ((HttpServletResponse) response).sendRedirect("/page/notfound");
            return;
        }

        /*
        处理缓存击穿问题：
        假如某一时刻，该缓存失效了，同时又有大量请求同时访问这个链接，那么请求会一下子达到后台数据库里
         */
        RLock lock = redissonClient.getLock(String.format(LOCK_SHORT_LINK_GOTO_KEY, fullShortUrl));
        lock.lock();
        try {

            // 双重锁判断：参考单利模式DCL写法
            originalUrl = stringRedisTemplate.opsForValue().get(String.format(SHORT_LINK_GOTO_KEY, fullShortUrl));
            if (StrUtil.isNotBlank(originalUrl)) {
                statsShortLink(null, fullShortUrl, request, response);
                // 302到原始链接
                ((HttpServletResponse) response).sendRedirect(originalUrl);
                return;
            }

            // 真正查询数据库之前，再尝试判断redis力是否存储了该短链接的空对象，以应对缓存穿透问题
            String empty = stringRedisTemplate.opsForValue().get(String.format(EMPTY_SHORT_LINK_KEY, fullShortUrl));
            if (StrUtil.isNotBlank(empty)) {
                ((HttpServletResponse) response).sendRedirect("/page/notfound");
                return;
            }

            // 去短链接路由表里查这个短链接对于的gid是哪个，因为我们在对t_link进行分表时选择的分片键是gid，所以这个得做一个中转
            LambdaQueryWrapper<LinkGotoDO> linkGotoLambdaQueryWrapper = Wrappers.lambdaQuery(LinkGotoDO.class)
                    .eq(LinkGotoDO::getFullShortUri, fullShortUrl);
            LinkGotoDO linkGotoDO = linkGotoMapper.selectOne(linkGotoLambdaQueryWrapper);
            if (linkGotoDO == null) {

                ((HttpServletResponse) response).sendRedirect("/page/notfound");

                // 为null的情况下缓存空对象(给这个也设置一个有效期，避免一直占用内存)
                stringRedisTemplate.opsForValue().set(String.format(EMPTY_SHORT_LINK_KEY, fullShortUrl), "-", DEFAULT_VALIDATE_TIME, TimeUnit.SECONDS);
                return;
            }

            // 获取原始链接
            LambdaQueryWrapper<ShortLinkDO> shortLinkLambdaQueryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                    .eq(ShortLinkDO::getGid, linkGotoDO.getGid())
                    .eq(ShortLinkDO::getFullShortUrl, fullShortUrl)
                    .eq(ShortLinkDO::getEnableStatus, 0)
                    .eq(ShortLinkDO::getDelFlag, 0);
            ShortLinkDO shortLinkDO = baseMapper.selectOne(shortLinkLambdaQueryWrapper);
            // 这里别忘了还得判断短链接有效期
            if (shortLinkDO == null || (shortLinkDO.getValidDate() != null && DateUtil.date().after(shortLinkDO.getValidDate()))) {
                ((HttpServletResponse) response).sendRedirect("/page/notfound");
                return;
            }

            // 将短链接和原始链接的关系存入 redis 缓存
            stringRedisTemplate.opsForValue().set(String.format(SHORT_LINK_GOTO_KEY, shortLinkDO.getFullShortUrl()), shortLinkDO.getOriginUrl());

            statsShortLink(shortLinkDO.getGid(), fullShortUrl, request, response);

            // 302到原始链接
            ((HttpServletResponse) response).sendRedirect(shortLinkDO.getOriginUrl());

        } finally {
            lock.unlock();
        }

    }

    /**
     * 短链接访问统计
     */
    private void statsShortLink(String gid, String fullShortUrl, ServletRequest request, ServletResponse response) {
        try {

            /*
            uv具体实现思路：
            用户访问短链接时，判断请求里是否存在我们颁发的uv这个cookie，存在，且这个cookie在redis里确实有，那么则说明之前访问过，那么uv不进行+1
             */
            AtomicBoolean uvFirstFlag = new AtomicBoolean();
            Cookie[] cookies = ((HttpServletRequest) request).getCookies();
            AtomicReference<String> uv = new AtomicReference<>();
            Runnable uvInitRunnable = () -> {
                // 否则不存在的话就颁发一个 uv 的 cookie
                uv.set(UUID.fastUUID().toString());
                Cookie uvCookie = new Cookie(UV_COOKIE_NAME, uv.get());
                // 设置访问这个短链接路径后缀是再携带这个cookie，得匹配对应
                uvCookie.setPath("localhost:8002/short-link/router" + StrUtil.sub(fullShortUrl, fullShortUrl.lastIndexOf("/"), fullShortUrl.length()));
                // 暂时设置 cookie 的有效期是一个月
                uvCookie.setMaxAge(30 * 24 * 60 * 60);
                ((HttpServletResponse) response).addCookie(uvCookie);

                uvFirstFlag.set(Boolean.TRUE);
                stringRedisTemplate.opsForSet().add(String.format(SHORT_LINK_STATS_UV, fullShortUrl), uv.get());
            };
            if (ArrayUtil.isNotEmpty(cookies)) {
                Arrays.stream(cookies)
                        .filter(each -> Objects.equals(each.getName(), UV_COOKIE_NAME))
                        .findFirst()
                        .map(Cookie::getValue)
                        .ifPresentOrElse(each -> {
                            // FIXME: 这里存在一个问题，我们通过这种方式去判断这个短链接是否被访问的话redis里会存非常多的这种key，而且由于没有设置过期时间，那会一直占用redis存储空间
                            // 如果存在这个 uv 的 cookie，那么再去redis里判断是否有这个短链接，把访问过的短链接存储到一个set集合里，通过add方法是否成功判断是否存在
                            uv.set(each);
                            Long uvAdd = stringRedisTemplate.opsForSet().add(String.format(SHORT_LINK_STATS_UV, fullShortUrl), each);
                            uvFirstFlag.set(uvAdd != null && uvAdd > 0);
                        }, uvInitRunnable);
            } else {
                uvInitRunnable.run();
            }

            /*
            uip统计实现思路，从请求中获取用户的真实访问ip地址，也是构建一个key-value，key是这个完整短链接，value记录已访问过的ip地址，add成功标识是新ip，否则就是旧ip
             */
            String clientIp = ShortLinkUtil.getClientIp((HttpServletRequest) request);
            Long uipAdd = stringRedisTemplate.opsForSet().add(String.format(SHORT_LINK_STATS_UIP, fullShortUrl), clientIp);
            boolean uipFirstFlag = (uipAdd != null && uipAdd > 0);

            if (StrUtil.isBlank(gid)) {
                LambdaQueryWrapper<LinkGotoDO> queryWrapper = Wrappers.lambdaQuery(LinkGotoDO.class)
                        .eq(LinkGotoDO::getFullShortUri, fullShortUrl);
                LinkGotoDO linkGotoDO = linkGotoMapper.selectOne(queryWrapper);
                gid = linkGotoDO.getGid();
            }

            DateTime date = DateUtil.date();
            int weekday = DateUtil.dayOfMonth(date);
            int hour = DateUtil.hour(date, true);
            LinkAccessStatsDO accessStatsDO = LinkAccessStatsDO.builder()
                    .gid(gid)
                    .fullShortUrl(fullShortUrl)
                    .uv(uvFirstFlag.get() ? 1 : 0)
                    .pv(1)
                    .uip(uipFirstFlag ? 1 : 0)
                    .date(date)
                    .weekday(weekday)
                    .hour(hour)
                    .build();
            linkAccessStatsMapper.statsShortLink(accessStatsDO);

            /*
            统计访问的地址数据
             */
            // 调用高德的服务
            // FIXME: 暂时使用hutool 的 http客户端，以后换，说是每次请求会造一个链接，性能低
            HashMap<String, Object> ipParamMap = new HashMap<>();
            ipParamMap.put("key", gaoDeIpServiceKey);
            ipParamMap.put("ip", clientIp);
            String responseBodyStr = HttpUtil.get(gaoDeIpServiceUrl, ipParamMap);
            JSONObject responseBodyJson = JSON.parseObject(responseBodyStr);
            if (Objects.equals(responseBodyJson.getString("infocode"), "10000")) {
                // 判断一个省是不是[]就能知道其他所以数据是不是未知了，不用判断所有的
                boolean unknowFlag = StrUtil.equals(responseBodyJson.getString("province"), "[]");
                LinkLocaleStatsDO linkLocaleStatsDO = LinkLocaleStatsDO.builder()
                        .fullShortUrl(fullShortUrl)
                        .gid(gid)
                        .date(date)
                        .cnt(1)
                        .province(unknowFlag ? "未知" : responseBodyJson.getString("province"))
                        .city(unknowFlag ? "未知" : responseBodyJson.getString("city"))
                        .adcode(unknowFlag ? "未知" : responseBodyJson.getString("adcode"))
                        .country("中国")
                        .build();
                linkLocaleStatsMapper.statsShortLinkLocale(linkLocaleStatsDO);
            }

            /*
            统计访问的操作系统
             */
            String os = ShortLinkUtil.getOperatingSystem((HttpServletRequest) request);
            LinkOsStatsDO linkOsStatsDO = LinkOsStatsDO.builder()
                    .fullShortUrl(fullShortUrl)
                    .gid(gid)
                    .date(date)
                    .cnt(1)
                    .os(os)
                    .build();
            linkOsStatsMapper.statsShortLinkOs(linkOsStatsDO);

            /*
            统计访问的浏览器信息
             */
            String browser = ShortLinkUtil.getBrowser((HttpServletRequest) request);
            LinkBrowserStatsDO linkBrowserStatsDO = LinkBrowserStatsDO.builder()
                    .fullShortUrl(fullShortUrl)
                    .gid(gid)
                    .date(date)
                    .cnt(1)
                    .browser(browser)
                    .build();
            linkBrowserStatsMapper.statsShortLinkBrowser(linkBrowserStatsDO);

            /*
             * 短链接访问设备统计
             */
            String device = ShortLinkUtil.getDevice((HttpServletRequest) request);
            LinkDeviceStatsDO linkDeviceStatsDO = LinkDeviceStatsDO.builder()
                    .fullShortUrl(fullShortUrl)
                    .gid(gid)
                    .date(date)
                    .cnt(1)
                    .device(device)
                    .build();
            linkDeviceStatsMapper.statsShortLinkDevice(linkDeviceStatsDO);

            /*
             * 短链接访问网络统计
             */
            String network = ShortLinkUtil.getNetwork((HttpServletRequest) request);
            LinkNetworkStatsDO linkNetworkStatsDO = LinkNetworkStatsDO.builder()
                    .fullShortUrl(fullShortUrl)
                    .gid(gid)
                    .date(date)
                    .cnt(1)
                    .network(network)
                    .build();
            linkNetworkStatsMapper.statsShortLinkNetwork(linkNetworkStatsDO);

            /*
            短链接访问日志信息记录
             */
            LinkAccessLogsDO linkAccessLogsDO = LinkAccessLogsDO.builder()
                    .user(uv.get())
                    .fullShortUrl(fullShortUrl)
                    .gid(gid)
                    .browser(browser)
                    .ip(clientIp)
                    .os(os)
                    .build();
            linkAccessLogsMapper.insert(linkAccessLogsDO);
        } catch (Exception ex) {
            log.error("统计短链接失败，失败原因：{}", ex);
        }
    }
}
